// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/android/jni_android.h"

#include <stddef.h>

#include <map>

#include "base/android/build_info.h"
#include "base/android/jni_string.h"
#include "base/android/jni_utils.h"
#include "base/lazy_instance.h"
#include "base/logging.h"

namespace {
using base::android::GetClass;
using base::android::MethodID;
using base::android::ScopedJavaLocalRef;

bool g_disable_manual_jni_registration = false;

JavaVM* g_jvm = NULL;
base::LazyInstance<base::android::ScopedJavaGlobalRef<jobject>>::Leaky
    g_class_loader
    = LAZY_INSTANCE_INITIALIZER;
jmethodID g_class_loader_load_class_method_id = 0;

} // namespace

namespace base {
namespace android {

    bool IsManualJniRegistrationDisabled()
    {
        return g_disable_manual_jni_registration;
    }

    void DisableManualJniRegistration()
    {
        DCHECK(!g_disable_manual_jni_registration);
        g_disable_manual_jni_registration = true;
    }

    JNIEnv* AttachCurrentThread()
    {
        DCHECK(g_jvm);
        JNIEnv* env = NULL;
        jint ret = g_jvm->AttachCurrentThread(&env, NULL);
        DCHECK_EQ(JNI_OK, ret);
        return env;
    }

    JNIEnv* AttachCurrentThreadWithName(const std::string& thread_name)
    {
        DCHECK(g_jvm);
        JavaVMAttachArgs args;
        args.version = JNI_VERSION_1_2;
        args.name = thread_name.c_str();
        args.group = NULL;
        JNIEnv* env = NULL;
        jint ret = g_jvm->AttachCurrentThread(&env, &args);
        DCHECK_EQ(JNI_OK, ret);
        return env;
    }

    void DetachFromVM()
    {
        // Ignore the return value, if the thread is not attached, DetachCurrentThread
        // will fail. But it is ok as the native thread may never be attached.
        if (g_jvm)
            g_jvm->DetachCurrentThread();
    }

    void InitVM(JavaVM* vm)
    {
        DCHECK(!g_jvm || g_jvm == vm);
        g_jvm = vm;
    }

    bool IsVMInitialized()
    {
        return g_jvm != NULL;
    }

    void InitReplacementClassLoader(JNIEnv* env,
        const JavaRef<jobject>& class_loader)
    {
        DCHECK(g_class_loader.Get().is_null());
        DCHECK(!class_loader.is_null());

        ScopedJavaLocalRef<jclass> class_loader_clazz = GetClass(env, "java/lang/ClassLoader");
        CHECK(!ClearException(env));
        g_class_loader_load_class_method_id = env->GetMethodID(class_loader_clazz.obj(),
            "loadClass",
            "(Ljava/lang/String;)Ljava/lang/Class;");
        CHECK(!ClearException(env));

        DCHECK(env->IsInstanceOf(class_loader.obj(), class_loader_clazz.obj()));
        g_class_loader.Get().Reset(class_loader);
    }

    ScopedJavaLocalRef<jclass> GetClass(JNIEnv* env, const char* class_name)
    {
        jclass clazz;
        if (!g_class_loader.Get().is_null()) {
            // ClassLoader.loadClass expects a classname with components separated by
            // dots instead of the slashes that JNIEnv::FindClass expects. The JNI
            // generator generates names with slashes, so we have to replace them here.
            // TODO(torne): move to an approach where we always use ClassLoader except
            // for the special case of base::android::GetClassLoader(), and change the
            // JNI generator to generate dot-separated names. http://crbug.com/461773
            size_t bufsize = strlen(class_name) + 1;
            char dotted_name[bufsize];
            memmove(dotted_name, class_name, bufsize);
            for (size_t i = 0; i < bufsize; ++i) {
                if (dotted_name[i] == '/') {
                    dotted_name[i] = '.';
                }
            }

            clazz = static_cast<jclass>(
                env->CallObjectMethod(g_class_loader.Get().obj(),
                    g_class_loader_load_class_method_id,
                    ConvertUTF8ToJavaString(env, dotted_name).obj()));
        } else {
            clazz = env->FindClass(class_name);
        }
        if (ClearException(env) || !clazz) {
            LOG(FATAL) << "Failed to find class " << class_name;
        }
        return ScopedJavaLocalRef<jclass>(env, clazz);
    }

    jclass LazyGetClass(
        JNIEnv* env,
        const char* class_name,
        base::subtle::AtomicWord* atomic_class_id)
    {
        static_assert(sizeof(subtle::AtomicWord) >= sizeof(jclass),
            "AtomicWord can't be smaller than jclass");
        subtle::AtomicWord value = base::subtle::Acquire_Load(atomic_class_id);
        if (value)
            return reinterpret_cast<jclass>(value);
        ScopedJavaGlobalRef<jclass> clazz;
        clazz.Reset(GetClass(env, class_name));
        subtle::AtomicWord null_aw = reinterpret_cast<subtle::AtomicWord>(NULL);
        subtle::AtomicWord cas_result = base::subtle::Release_CompareAndSwap(
            atomic_class_id,
            null_aw,
            reinterpret_cast<subtle::AtomicWord>(clazz.obj()));
        if (cas_result == null_aw) {
            // We intentionally leak the global ref since we now storing it as a raw
            // pointer in |atomic_class_id|.
            return clazz.Release();
        } else {
            return reinterpret_cast<jclass>(cas_result);
        }
    }

    template <MethodID::Type type>
    jmethodID MethodID::Get(JNIEnv* env,
        jclass clazz,
        const char* method_name,
        const char* jni_signature)
    {
        jmethodID id = type == TYPE_STATIC ? env->GetStaticMethodID(clazz, method_name, jni_signature) : env->GetMethodID(clazz, method_name, jni_signature);
        if (base::android::ClearException(env) || !id) {
            LOG(FATAL) << "Failed to find " << (type == TYPE_STATIC ? "static " : "") << "method " << method_name << " " << jni_signature;
        }
        return id;
    }

    // If |atomic_method_id| set, it'll return immediately. Otherwise, it'll call
    // into ::Get() above. If there's a race, it's ok since the values are the same
    // (and the duplicated effort will happen only once).
    template <MethodID::Type type>
    jmethodID MethodID::LazyGet(JNIEnv* env,
        jclass clazz,
        const char* method_name,
        const char* jni_signature,
        base::subtle::AtomicWord* atomic_method_id)
    {
        static_assert(sizeof(subtle::AtomicWord) >= sizeof(jmethodID),
            "AtomicWord can't be smaller than jMethodID");
        subtle::AtomicWord value = base::subtle::Acquire_Load(atomic_method_id);
        if (value)
            return reinterpret_cast<jmethodID>(value);
        jmethodID id = MethodID::Get<type>(env, clazz, method_name, jni_signature);
        base::subtle::Release_Store(
            atomic_method_id, reinterpret_cast<subtle::AtomicWord>(id));
        return id;
    }

    // Various template instantiations.
    template jmethodID MethodID::Get<MethodID::TYPE_STATIC>(
        JNIEnv* env, jclass clazz, const char* method_name,
        const char* jni_signature);

    template jmethodID MethodID::Get<MethodID::TYPE_INSTANCE>(
        JNIEnv* env, jclass clazz, const char* method_name,
        const char* jni_signature);

    template jmethodID MethodID::LazyGet<MethodID::TYPE_STATIC>(
        JNIEnv* env, jclass clazz, const char* method_name,
        const char* jni_signature, base::subtle::AtomicWord* atomic_method_id);

    template jmethodID MethodID::LazyGet<MethodID::TYPE_INSTANCE>(
        JNIEnv* env, jclass clazz, const char* method_name,
        const char* jni_signature, base::subtle::AtomicWord* atomic_method_id);

    bool HasException(JNIEnv* env)
    {
        return env->ExceptionCheck() != JNI_FALSE;
    }

    bool ClearException(JNIEnv* env)
    {
        if (!HasException(env))
            return false;
        env->ExceptionDescribe();
        env->ExceptionClear();
        return true;
    }

    void CheckException(JNIEnv* env)
    {
        if (!HasException(env))
            return;

        // Exception has been found, might as well tell breakpad about it.
        jthrowable java_throwable = env->ExceptionOccurred();
        if (java_throwable) {
            // Clear the pending exception, since a local reference is now held.
            env->ExceptionDescribe();
            env->ExceptionClear();

            // Set the exception_string in BuildInfo so that breakpad can read it.
            // RVO should avoid any extra copies of the exception string.
            base::android::BuildInfo::GetInstance()->SetJavaExceptionInfo(
                GetJavaExceptionInfo(env, java_throwable));
        }

        // Now, feel good about it and die.
        LOG(FATAL) << "Please include Java exception stack in crash report";
    }

    std::string GetJavaExceptionInfo(JNIEnv* env, jthrowable java_throwable)
    {
        ScopedJavaLocalRef<jclass> throwable_clazz = GetClass(env, "java/lang/Throwable");
        jmethodID throwable_printstacktrace = MethodID::Get<MethodID::TYPE_INSTANCE>(
            env, throwable_clazz.obj(), "printStackTrace",
            "(Ljava/io/PrintStream;)V");

        // Create an instance of ByteArrayOutputStream.
        ScopedJavaLocalRef<jclass> bytearray_output_stream_clazz = GetClass(env, "java/io/ByteArrayOutputStream");
        jmethodID bytearray_output_stream_constructor = MethodID::Get<MethodID::TYPE_INSTANCE>(
            env, bytearray_output_stream_clazz.obj(), "<init>", "()V");
        jmethodID bytearray_output_stream_tostring = MethodID::Get<MethodID::TYPE_INSTANCE>(
            env, bytearray_output_stream_clazz.obj(), "toString",
            "()Ljava/lang/String;");
        ScopedJavaLocalRef<jobject> bytearray_output_stream(env,
            env->NewObject(bytearray_output_stream_clazz.obj(),
                bytearray_output_stream_constructor));

        // Create an instance of PrintStream.
        ScopedJavaLocalRef<jclass> printstream_clazz = GetClass(env, "java/io/PrintStream");
        jmethodID printstream_constructor = MethodID::Get<MethodID::TYPE_INSTANCE>(
            env, printstream_clazz.obj(), "<init>",
            "(Ljava/io/OutputStream;)V");
        ScopedJavaLocalRef<jobject> printstream(env,
            env->NewObject(printstream_clazz.obj(), printstream_constructor,
                bytearray_output_stream.obj()));

        // Call Throwable.printStackTrace(PrintStream)
        env->CallVoidMethod(java_throwable, throwable_printstacktrace,
            printstream.obj());

        // Call ByteArrayOutputStream.toString()
        ScopedJavaLocalRef<jstring> exception_string(
            env, static_cast<jstring>(env->CallObjectMethod(bytearray_output_stream.obj(), bytearray_output_stream_tostring)));

        return ConvertJavaStringToUTF8(exception_string);
    }

} // namespace android
} // namespace base
